Analiza zbioru Estimation of Obesity Levels Based On Eating Habits and Physical Condition

Author

Piotr Biesiada, Miłosz Grabarski, Daniel Mokrzan

Published

February 8, 2025

Opis zestawu danych

Zestaw danych został skompletowany na podstawie 16 pytań w ankiecie przeprowadzonej na terytorium Meksyku, Peru i Kolumbii na grupie 485 osób. Dla danych pochodzących z ankiet obliczono \[ BMI = \frac{Weight}{Height^2}, \] a następnie skategoryzowane je w następujący sposób:

  • Underweight: BMI Less than 18.5
  • Normal: BMI 18.5 to 24.9
  • Overweight I: BMI 25.0 to 26.9
  • Overweight: BMI 27.0 to 29.9
  • Obesity I: BMI 30.0 to 34.9
  • Obesity II: BMI 35.0 to 39.9
  • Obesity III: BMI Higher than 40

Podstawowe statystyki

Tabela z podstawowymi statystykami

Przebadano 485 respondentów. Średni wiek badanych osób to 23 lata. Osoby te wypijają średnio 2 litry wody dziennie, zawsze spożywają dziennie co najmniej jedno warzywo. Co czwarta osoba nie uprawia żadnej aktywności fiznycznej, a średnie BMI to 24.34. Średnia wartośc BMI dla próbki badanych mieści się w górnej granicy prawidłowego stosunku wagi do wzrostu.

Age Height Weight vegetables_consumption number_of_main_meals daily_water_consumption physical_activity time_using_technology BMI
count 485.000000 485.000000 485.000000 485.000000 485.000000 485.000000 485.000000 485.000000 485.000000
mean 23.162887 1.685216 69.552165 2.325773 2.657732 1.927835 1.164948 0.661856 24.341385
std 6.718380 0.097454 17.041178 0.590083 0.923951 0.677980 1.023287 0.722347 4.787519
min 14.000000 1.450000 39.000000 1.000000 1.000000 1.000000 0.000000 0.000000 13.291588
25% 19.000000 1.610000 58.000000 2.000000 3.000000 1.000000 0.000000 0.000000 21.007668
50% 21.000000 1.680000 67.000000 2.000000 3.000000 2.000000 1.000000 1.000000 23.711845
75% 24.000000 1.750000 80.000000 3.000000 3.000000 2.000000 2.000000 1.000000 26.672763
max 61.000000 1.980000 173.000000 3.000000 4.000000 3.000000 3.000000 2.000000 49.472390

Przynależność do kategorii BMI

Kategorie BMI Liczba wystąpień
0 Insufficient_Weight 35
1 Normal_Weight 276
2 Overweight_Level_I 62
3 Overweight_Level_II 52
4 Obesity_Type_I 46
5 Obesity_Type_II 11
6 Obesity_Type_III 3

Histogramy i gęstości dla zmiennych ilościowych

Boxplot

Wykres pudełkowy dla zmiennej Age. Widzimy, że starzenie się społeczeństwa wpływa na postępującą otyłość:

Code
plot = sns.boxplot(data=df, y='Age', hue='BMI_classification', hue_order=custom_order)
plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left', borderaxespad=0)
plot.set_title("Boxplot zmiennej Age w podziale na kategorie wagowe")
Text(0.5, 1.0, 'Boxplot zmiennej Age w podziale na kategorie wagowe')

Plot - czynniki żywieniowe

Code

#| echo: true
#| output: true
fig, ax = plt.subplots(figsize=(7,5))
sns.histplot(data=df, x='BMI_classification', hue='alcohol_consumption', multiple="fill",
             discrete=True, shrink=0.8, stat="percent")
ax.set_title('Podział na Alcohol Consumption')
sns.move_legend(ax, "upper left", bbox_to_anchor=(1, 1))
plt.xlabel('BMI Classification')  
plt.ylabel('Procent')            
plt.xticks(rotation=90)
plt.tight_layout()
plt.show()

Code
fig, ax = plt.subplots(figsize=(7,5))
# Warzywa
sns.histplot(data=df, x='BMI_classification', hue='vegetables_consumption', multiple="fill",
             discrete=True, shrink=0.8, stat="percent")
ax.set_title('Podział na liczbę spożywanych warzyw') 
sns.move_legend(ax, "upper left", bbox_to_anchor=(1, 1))
plt.xlabel('BMI Classification')  
plt.ylabel('Procent')            
plt.xticks(rotation=90)
plt.tight_layout()
plt.show()

Code
fig, ax = plt.subplots(figsize=(7,5))

# Główne posiłki
sns.histplot(data=df, x='BMI_classification', hue='number_of_main_meals', multiple="fill",
             discrete=True, shrink=0.8, stat="percent")
ax.set_title('Podział na liczbę spożywanych posiłków')
sns.move_legend(ax, "upper left", bbox_to_anchor=(1, 1))
plt.xlabel('BMI Classification')  
plt.ylabel('Procent')            
plt.xticks(rotation=90)
plt.tight_layout()
plt.show()

Code
fig, ax = plt.subplots(figsize=(7,5))

# Częstość podjadania
ax = sns.histplot(data=df, x='BMI_classification', hue='consumption_between_meals', multiple="fill",
             discrete=True, shrink=0.8, stat="percent", ax = ax)
ax.set_title('Podział na liczbę spożywanych przekąsek (podjadanie)')
sns.move_legend(ax, "upper left", bbox_to_anchor=(1, 1))
plt.xlabel('BMI Classification')  
plt.ylabel('Procent')            
plt.xticks(rotation=90)
plt.tight_layout()
plt.show()

Model uczenia maszynowego

Model XGBoost

Wykorzystano wzmacniane gradientowo drzewa decyzyjne z użyciem biblioteki XGBoost do rozwiązania zagadnienia klasyfikacyjnego stopnii otyłości. Zmienną objaśnianą były kategorie BMI, zmiennymi objaśniającymi pozostałe atrybuty tabeli z pominięciem wzrostu, masy i wskaźnika BMI. Wzrost i masa zostały pominięte jako, że są bezpośrednio skorelowane ze wskaźnikiem i kategorią BMI ze względu na wzór je wyznaczający, zaś sama kategoria wynika ze wskaźnika. Użycie tych trzech atrybutów zapewne pozwoliło uzyskać bardzo wysoką dokładność modelu, jakkolwiek nie miałby żadnego sensu w aspekcie wnioskowania statystycznego.

W pierwszym kroku zainicjowano niezbędne biblioteki, zaczytano dane z odrzuceniem kolumn, które nie brału udziały w uczeniu maszynowym. Zmienna objaśniania została przypisana słownikowo do cyfr z zakresu <0:6>. Pozostałe kolumny kategoryczne zostały zetykietowane z użyciem metody LabelEncoder z biblioteki sklearn. Model trenowany był z wykorzystaniem walidacji krzyżowej z podziałem na 10 podzbiorów - 9 treningowych i 1 testowy w każdej turze.

Code
import numpy as np
import pandas as pd
from sklearn.model_selection import cross_val_predict, StratifiedKFold
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import classification_report, accuracy_score
from xgboost import XGBClassifier

data = pd.read_csv("obesity_clear.csv", usecols=lambda x: x not in ['Height', 'Weight', 'BMI'], index_col=0)
bmi_mapping = {
    'Insufficient_Weight': 0,
    'Normal_Weight': 1,
    'Overweight_Level_I': 2,
    'Overweight_Level_II': 3,
    'Obesity_Type_I': 4,
    'Obesity_Type_II': 5,
    'Obesity_Type_III': 6
}
reverse_bmi_mapping = {v: k for k, v in bmi_mapping.items()}
data['BMI_classification'] = data['BMI_classification'].map(bmi_mapping)

label_encoder = LabelEncoder()
categorical_columns = [
    'Gender',
    'overweight_in_family',
    'high_cal_food',
    'consumption_between_meals',
    'is_smoking',
    'calories_monitoring',
    'alcohol_consumption',
    'prefered_transportation'
]
for col in categorical_columns:
    data[col] = label_encoder.fit_transform(data[col])

X = data.drop(columns=['BMI_classification'])
y = data['BMI_classification']

xgb_model = XGBClassifier(
    objective='multi:softmax',
    num_class=len(y.unique()),
    eval_metric='mlogloss',
    random_state=42,
)

skf = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)
y_pred = cross_val_predict(xgb_model, X, y, cv=skf)

print("\nDetailed Classification Report (10-fold CV):")
print(classification_report(y, y_pred,
                          target_names=[reverse_bmi_mapping[i] for i in range(len(bmi_mapping))]))

fold_metrics = []
train_accuracies = []
test_accuracies = []

for fold_idx, (train_idx, val_idx) in enumerate(skf.split(X, y), 1):
    X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]
    y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]
    
    # Fit model and make predictions
    xgb_model.fit(X_train, y_train)
    
    # Calculate training accuracy
    y_train_pred = xgb_model.predict(X_train)
    train_acc = accuracy_score(y_train, y_train_pred)
    train_accuracies.append(train_acc)
    
    # Calculate validation accuracy and other metrics
    y_val_pred = xgb_model.predict(X_val)
    test_acc = accuracy_score(y_val, y_val_pred)
    test_accuracies.append(test_acc)
    
    fold_report = classification_report(y_val, y_val_pred,
                                      target_names=[reverse_bmi_mapping[i] for i in range(len(bmi_mapping))],
                                      output_dict=True)
    fold_metrics.append(fold_report)

print("\nPer-category metrics across folds (mean ± std):")
categories = [reverse_bmi_mapping[i] for i in range(len(bmi_mapping))]
for category in categories:
    precision_scores = [fold[category]['precision'] for fold in fold_metrics]
    recall_scores = [fold[category]['recall'] for fold in fold_metrics]
    f1_scores = [fold[category]['f1-score'] for fold in fold_metrics]
    support = fold_metrics[0][category]['support']
    print(f"\n{category}:")
    print(f"Precision: {np.mean(precision_scores):.4f}{np.std(precision_scores):.4f})")
    print(f"Recall: {np.mean(recall_scores):.4f}{np.std(recall_scores):.4f})")
    print(f"F1-score: {np.mean(f1_scores):.4f}{np.std(f1_scores):.4f})")
    print(f"Support: {support}")

print(f"\nTraining Accuracy: {np.mean(train_accuracies):.4f}{np.std(train_accuracies):.4f})")
print(f"Test Accuracy: {np.mean(test_accuracies):.4f}{np.std(test_accuracies):.4f})")

Detailed Classification Report (10-fold CV):
                     precision    recall  f1-score   support

Insufficient_Weight       0.82      0.85      0.84       271
      Normal_Weight       0.64      0.60      0.62       300
 Overweight_Level_I       0.68      0.68      0.68       285
Overweight_Level_II       0.71      0.64      0.67       281
     Obesity_Type_I       0.78      0.78      0.78       368
    Obesity_Type_II       0.76      0.87      0.81       338
   Obesity_Type_III       0.93      0.88      0.90       268

           accuracy                           0.76      2111
          macro avg       0.76      0.76      0.76      2111
       weighted avg       0.76      0.76      0.76      2111


Per-category metrics across folds (mean ± std):

Insufficient_Weight:
Precision: 0.8253 (±0.0559)
Recall: 0.8485 (±0.0674)
F1-score: 0.8358 (±0.0562)
Support: 27.0

Normal_Weight:
Precision: 0.6433 (±0.1060)
Recall: 0.5967 (±0.1059)
F1-score: 0.6162 (±0.0997)
Support: 30.0

Overweight_Level_I:
Precision: 0.6831 (±0.0754)
Recall: 0.6847 (±0.0581)
F1-score: 0.6824 (±0.0596)
Support: 29.0

Overweight_Level_II:
Precision: 0.7102 (±0.0751)
Recall: 0.6406 (±0.0717)
F1-score: 0.6726 (±0.0691)
Support: 28.0

Obesity_Type_I:
Precision: 0.7817 (±0.0368)
Recall: 0.7824 (±0.0537)
F1-score: 0.7809 (±0.0348)
Support: 37.0

Obesity_Type_II:
Precision: 0.7648 (±0.0547)
Recall: 0.8728 (±0.0509)
F1-score: 0.8141 (±0.0445)
Support: 34.0

Obesity_Type_III:
Precision: 0.9314 (±0.0460)
Recall: 0.8808 (±0.0215)
F1-score: 0.9048 (±0.0263)
Support: 27.0

Training Accuracy: 0.9526 (±0.0021)
Test Accuracy: 0.7594 (±0.0187)

Widoczne jest przeuczenie modelu - uśrednione wyniki na zbiorach treningowych są o ~20% lepsze niż na zbiorach testowych. Najgorsze wyniki model osiąga w klasyfikowaniu wagi normalnej oraz lekkiej nadwagi. Najlepsze wyniki osiąga w kategoryzowaniu najwyższego stopnia otyłości. Aby dokładniej przeanalizować wyniki modelu, wygenerowany został confusion matrix.

Code
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix

cm = confusion_matrix(y, y_pred)

plt.figure(figsize=(12, 10))

sns.heatmap(cm,
            annot=True,
            fmt='d',
            cmap='Reds',
            xticklabels=[reverse_bmi_mapping[i] for i in range(len(bmi_mapping))],
            yticklabels=[reverse_bmi_mapping[i] for i in range(len(bmi_mapping))])

plt.xticks(rotation=90, ha='right')
plt.yticks(rotation=0)

plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.title('Confusion Matrix - Weight Classification')

plt.tight_layout()

plt.show()

Confusion matrix wskazuje, że model ma problem z precyzyjnym wskazaniem konkretnej kategorii BMI. Najczętszym błędem jest niepoprawne wskazanie najbliższego sąsiada. Wywnioskować z tego można, że cechy w analizowanym zestawie danych są niewystarczające do tak dokładnej klasyfikacji. Szczególnie widoczne jest to w przypadku wagi prawidłowej i lekkiej nadwagi gdzie wyniki wskazują na to, że istnieje więcej subtelnych zależności mogących prowadzić do zakwalifowania do którejś z tych klasyfikcji. Ponadto zgodnie z analizą literaturą sam wskaźnik BMI jest wysoce obciążony niedokładnością ze względu na jego “prostotę”. Bierze on pod uwagę wyłącznie wzrost i masę ciała. Jest on w związku z tym nieprzydatny w przypadku ludzi czynnie uczęszczających na siłownię i budujących masę mięśniową - gdzie nieprawidłowo w ich przypadku jest wskazywana nadwaga albo otyłość. Dla poprawy modelu sugerowane jest więc dodanie dodatkowych cech do modelu umożliwiających poprawną identyfikację bardziej subtelnych wzorców.

Końcowo wygenerowano jeszcze wykres słupkowy współczynników cech modelu by zidentyfikować te zmienne objaśniające, którą miały największą wagę przy predykcji

Code
feature_importance = pd.DataFrame({
    'Feature': X.columns,
    'Importance': xgb_model.feature_importances_
})

feature_importance = feature_importance.sort_values('Importance', ascending=True)

plt.figure(figsize=(12, 8))
sns.barplot(
    data=feature_importance,
    y='Feature',
    x='Importance',
    hue = 'Feature',
    palette='viridis'
)

plt.title('Feature Importance in Weight Classification', pad=20, size=14)
plt.xlabel('Importance Score', size=12)
plt.ylabel('Features', size=12)

plt.grid(True, axis='x', linestyle='--', alpha=0.6)

plt.tight_layout()

plt.show()

print("\nFeature Importance Scores:")
for idx, row in feature_importance.iterrows():
    print(f"{row['Feature']}: {row['Importance']:.4f}")


Feature Importance Scores:
is_smoking: 0.0313
time_using_technology: 0.0343
physical_activity: 0.0350
daily_water_consumption: 0.0357
calories_monitoring: 0.0502
Age: 0.0558
high_cal_food: 0.0609
alcohol_consumption: 0.0687
number_of_main_meals: 0.0713
prefered_transportation: 0.0862
vegetables_consumption: 0.0876
consumption_between_meals: 0.1002
Gender: 0.1051
overweight_in_family: 0.1776